﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Execution
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using ConnectDebug;
    using Microsoft.Linq.Expressions;
    using Microsoft.Scripting;
    using ConnectDebug.Internal;

    // TODO: VBA doesn't support multiple functions with the same name, but we should.
    // TODO: Option Explicit off support 

    internal class LocalDefinition
    {
        public readonly string Name;
        public readonly Type Type;

        public LocalDefinition(string name, Type type)
        {
            this.Name = name;
            this.Type = type;
        }
    }

    /// <summary>
    /// Represents a standard code module.
    /// </summary>
    public sealed class BasicModule : IDynamicMetaObjectProvider
    {
        private string mName;
        private IEnumerable<BasicFunction> mFunctions;
        private Dictionary<string, BasicFunction> mFunctionsByNameCI = new Dictionary<string, BasicFunction>(StringComparer.OrdinalIgnoreCase);
        private Dictionary<string, BasicFunction> mFunctionsByNameCS = new Dictionary<string, BasicFunction>(StringComparer.Ordinal);

        private ProxyDebugger mDebugProxy;

        // TODO: Allocate strongly-typed storage to avoid boxing?
        private Dictionary<string, object> mLocalsByNameCI = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        private Dictionary<string, object> mLocalsByNameCS = new Dictionary<string, object>(StringComparer.Ordinal);

        #region .ctor

        /// <summary>
        /// Inititializes a new instance of the <see cref="BasicModule" /> class.
        /// </summary>
        /// <param name="name">The name of the module.</param>
        /// <param name="functions">The functions which belong to the module.</param>
        /// <param name="locals">The declared variables which belong to the module</param>
        /// <param name="debugger">The debugger for the module if there is one.</param>
        internal BasicModule(string name, IEnumerable<BasicFunction> functions, IEnumerable<LocalDefinition> locals, ProxyDebugger debugger)
        {
            mName = name;
            mFunctions = functions;
            mDebugProxy = debugger;

            // Take ownership of the functions and build 
            // a dictionary for fast member lookup
            foreach (BasicFunction function in functions)
            {

                // Take ownership
                function.SetOwner(this);

                // Construct dictionary (case sensitive & insensitive)
                mFunctionsByNameCS.Add(function.Name, function);
                mFunctionsByNameCI.Add(function.Name, function);
            }

            // Allocate locals
            foreach (var localDef in locals)
            {
                mLocalsByNameCI.Add(localDef.Name, null);
                mLocalsByNameCS.Add(localDef.Name, null);
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Determines whether the module is debuggable.
        /// </summary>
        public bool IsDebuggable
        {
            get { return mDebugProxy != null; }
        }

        /// <summary>
        /// Determines whether a debugger is attached.
        /// </summary>
        public bool IsDebuggerAttached
        {
            get { return IsDebuggable && mDebugProxy.Debugger != null; }
        }

        /// <summary>
        /// Returns the debugging information for the module if it was compiled with
        /// debugging information.
        /// </summary>
        public DebugInfo DebugInfo
        {
            get { return mDebugProxy == null ? null : mDebugProxy.DebugInfo; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Tries to attach a debugger to the module.
        /// </summary>
        /// <param name="debugger">The debugger to attach.</param>
        public void AttachDebugger(IInternalDebugger debugger)
        {
            if (debugger == null) throw new ArgumentNullException("debugger");
            if (!IsDebuggable) throw new InvalidOperationException();

            mDebugProxy.Debugger = debugger;
        }

        /// <summary>
        /// Gets a read-only collection of defined functions.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<BasicFunction> GetFunctions()
        {
            return new ReadOnlyCollection<BasicFunction>(new List<BasicFunction>(mFunctions));
        }

        /// <summary>
        /// Gets a function with the given name by using a case-insensitive lookup.
        /// </summary>
        /// <param name="name">The name of the function to get.</param>
        /// <returns></returns>
        public BasicFunction GetFunction(string name)
        {
            return this.GetFunction(name, true);
        }

        /// <summary>
        /// Gets a function with the given name.
        /// </summary>
        /// <param name="name">The name of the function to get.</param>
        /// <param name="ignoreCase">True to ignore case, otherwise false.</param>
        /// <returns></returns>
        public BasicFunction GetFunction(string name, bool ignoreCase)
        {
            BasicFunction result;
            IDictionary<string, BasicFunction> dict = ignoreCase ? mFunctionsByNameCI : mFunctionsByNameCS;

            if (dict.TryGetValue(name, out result))
            {
                return result;
            }
            else
            {
                return null;
            }
        }

        #endregion

        #region Internal Methods

        internal bool TryGetLocalValue(string localName, out object value)
        {
            return mLocalsByNameCI.TryGetValue(localName, out value);
        }

        internal void SetLocalValue(string localName, object value)
        {
            mLocalsByNameCI[localName] = value;
            mLocalsByNameCS[localName] = value;
        }

        #endregion

        #region IDynamicObject Members

        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
        {
            return new BasicModuleMetaObject(parameter, this);
        }

        #endregion

        #region BasicModuleMetaObject

        private class BasicModuleMetaObject : DynamicMetaObject
        {
            private BasicModule mModule;

            public BasicModuleMetaObject(Expression parameter, BasicModule value)
                : base(parameter, BindingRestrictions.Empty, value)
            {
                mModule = value;
            }

            public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
            {
                return base.BindInvokeMember(binder, args);
            }

            public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
            {
                Dictionary<string, BasicFunction> membersByName = binder.IgnoreCase ? mModule.mFunctionsByNameCI : mModule.mFunctionsByNameCS;
                BasicFunction matchingMember;

                // Look for a member with the given name
                if (membersByName.TryGetValue(binder.Name, out matchingMember))
                {
                    return new DynamicMetaObject(
                        Expression.Constant(matchingMember),
                        BindingRestrictions.Empty
                        );
                }

                return base.BindGetMember(binder);
            }
        }

        #endregion
    }
}
